/**
*  \file     li_ion_sdo.c
*
*  \brief    Lithium-Ion battery management system SDO transfer module
*
*  \author   Ralf Huber <ralf.huber@linde-mh.de>
*
*/

/*===============================================[ private includes  ]================================================*/
#include "li_ion_sdo.h"

#include <string.h>                    /* memcpy */

//#include "../cfg/lithium_ion_cfg.h"    /* Configuration       */
#include "../cosdoc/cosdoc.h"          /* CANopen SDO client handler */
#include "../fdmem/fdmem.h"            /* fdmem objects for SDO client data buffers (cosdoc) */
#include "../sched/sched.h"            /* for creating tasklets and assigning them to the scheduler */
#include "../cancom/can_li_ion_bms.h"

/*===============================================[ private defines   ]================================================*/

/* LI_ION_SDO_MAX_OBJECT_SIZE must be >= 4 (at least) in order to store potential abort codes from the SDO server */
#if LI_ION_SDO_MAX_OBJECT_SIZE < 4
#define LI_ION_SDO_MAX_OBJECT_SIZE 4
#endif

/* These defines specify the SDO object IDs that are provided by the lithium-ion battery management SDO server */
/* numbers are taken from the SDO object dictionary provided by BMZ */
#define LI_ION_SDO_ID_BATTERY_SERIAL_NO   U32_CONST(0x224101)
#define LI_ION_SDO_ID_NOMINAL_VOLTAGE     U32_CONST(0x227001)
#define LI_ION_SDO_ID_VIN                 U32_CONST(0x227101)
#define LI_ION_SDO_ID_TURN_BATTERY_ON     U32_CONST(0x220001)
#define LI_ION_SDO_ID_TURN_BATTERY_OFF    U32_CONST(0x220002)
#define LI_ION_SDO_ID_RESET_BATTERY       U32_CONST(0x221001)
#define LI_ION_SDO_ID_ENERGY_CONTENT      U32_CONST(0x224301)
#define LI_ION_SDO_ID_CAN_SCHEME          U32_CONST(0x300C01)
#define LI_ION_SDO_ID_SOFTWARE_VARIANT    U32_CONST(0x225001)
#define LI_ION_SDO_ID_HARDWARE_VARIANT    U32_CONST(0x225101)
/* ... more SDO objects are available but they are not yet needed ... */

/* this will be specified as abort code in the post-transmit callback if the transfer was not aborted */
#define LI_ION_SDO_NO_ABORT_CODE          U32_CONST(0)

/* SDO transfer timeout detection runs in the 10 ms task and it uses 'ticks' instead of absolute times */
#define LI_ION_SDO_TIMEOUT_100MS_TICK     U32_CONST(1)      /* each run of the 10 ms task corresponds to 1 tick */
#define LI_ION_SDO_TIMEOUT_100MS          U32_CONST(10)     /* timeout condition (100 ms) corresponds to 10 ticks */

/*===============================================[ private datatypes ]================================================*/

/* This enum contains states for the Li-Ion SDO transfer module. */
typedef enum {
   SDO_IDLE,            /*<! no Li-Ion SDO transfer is currently in progress */
   SDO_UPLOAD,          /*<! an SDO upload is currently in progress */
   SDO_DOWNLOAD         /*<! an SDO download is currently in progress */
} li_ion_sdo_progress_t;

/*===============================================[ private variables ]================================================*/

/* This is the SDO client object that will perform all SDO protocol tasks */
static struct cosdoc li_ion_sdo_client = {0};

/* memory buffer for sent and received SDO objects */
static U8 li_ion_sdo_memory[LI_ION_SDO_MAX_OBJECT_SIZE] = {0};

/* fdmem object that encapsulates the 'li_ion_sdo_memory' buffer so it can be used with the cosdoc unit */
static struct fdmem li_ion_sdo_memory_handle = {{0}};    /* double braces to fix GCC bug 53119 */

/* tasklets that handle the SDO transfers */
static struct tasklet li_ion_sdo_idle_tasklet = {0};     /* idle task processes incoming SDO messages */
static struct tasklet li_ion_sdo_10ms_tasklet = {0};     /* 10 ms task cleans up after transfers are finished */

/* the following variables store information which is necessary after an SDO transfer is completed */
static li_ion_sdo_callback_t finishing_callback = NULL;           /* callback function which is called after an SDO transfer  */
static li_ion_sdo_progress_t current_transfer_type = SDO_IDLE;    /* what kind of SDO transfer is currently in progress?      */
static U8* upload_destination = NULL;                             /* where should the uploaded data be stored                 */
static U8 upload_destination_size = U8_CONST(0);                  /* how big is 'upload_destination'                          */
static BO upload_size_exact = FALSE;                              /* do we expect exactly 'upload_destination_size' bytes     */

/* Some functions below need a pointer to an U8 with a value of 1 (for example 'li_ion_sdo_turn_battery_on'). */
static const U8 li_ion_sdo_one = U8_CONST(1);

/*===============================================[ public variables  ]================================================*/

/*===============================================[ private functions ]================================================*/

/*
* Transmit SDO requests from the SDO client
*
* This function will be called by the lithium-ion cosdoc object (SDO client) when it wants to send an SDO request. It
* copies the contents of the request message to the message buffer of the CAN driver and adds the CAN message to the
* transmission queue.
*
* \param   data        Contains whatever pointer was passed to 'cosdoc_initialize' as parameter 'callback_data'.
* \param   msg         This is the content of the SDO message that is supposed to be transmitted via CAN.
* \return  RC_SUCCESS  always
*/
RC li_ion_sdo_client_transmit(void *data, U8 msg[8])
{
   UNREFERENCED(data);

   can_ext_tx_process_liloBMS_sdo(msg);
   return RC_SUCCESS;
}

/*
* This function runs in the idle task and processes SDO messages sent to the traction node.
*
* \param task  Tasklet structure for the idle task
*/
void li_ion_sdo_idle_callback (struct tasklet *task)
{
   UNREFERENCED(task);

   /* call SDO client idle service to process incoming messages */
   cosdoc_idle_service(&li_ion_sdo_client);
}

/*
* This function finalizes an SDO transfer by calling the post-transmit callback (if it is not NULL). The type of the
* current SDO transfer is then set to SDO_IDLE, so a new SDO transfer can be started afterwards.
*
* \param return_code    Return code that will be passed along to the post-transmit callback.
* \param upload_size    Size of the uploaded SDO data, which will be passed along to the post-transmit callback.
* \param abort_code     Abort code that will be passed along to the post-transmit callback.
*/
void li_ion_sdo_finalize_transfer(const RC return_code, const U32 return_size, const U32 abort_code)
{
   upload_destination = NULL;
   upload_destination_size = 0;
   upload_size_exact = FALSE;

   if (NULL != finishing_callback)
   {
      finishing_callback(return_code, return_size, abort_code);
      finishing_callback = NULL;
   }

   current_transfer_type = SDO_IDLE;
}

/*
 * This function is called if an SDO transfer was successfully completed. In case the transfer was an SDO upload, it
 * copies the received data to the application-defined buffer ('upload_destination'), but only if the buffer exists and
 * is large enough for the received data. Afterwards, it calls the application-defined post-transfer callback function.
 */
STATIC_INLINE void li_ion_sdo_finalize_completed_transfer()
{
   if (SDO_UPLOAD == current_transfer_type)
   {
      U32 upload_size = li_ion_sdo_client.size;          /* how many bytes did we receive? */

      /* generate error if an exact amount of uploaded bytes is expected but a different amount was received */
      if ( (TRUE == upload_size_exact) && (upload_destination_size != upload_size) )
      {
         U32 sdo_abort_code = COSDO_ABORT_SERVICE_LENGTH_PARAMETER_DOES_NOT_MATCH;

         if (upload_destination_size > upload_size)
         {
            sdo_abort_code = COSDO_ABORT_DATA_TYPE_DOES_NOT_MATCH_LENGTH_OF_SERVICE_PARAMETER_TOO_LOW;
         }
         else if (upload_destination_size < upload_size)
         {
            sdo_abort_code = COSDO_ABORT_DATA_TYPE_DOES_NOT_MATCH_LENGTH_OF_SERVICE_PARAMETER_TOO_HIGH;
         }
         else
         {
            /* this will never be reached */
         }

         /* send SDO abort request to SDO server and tell the application about the wrong parameter size */
         cosdoc_abort(&li_ion_sdo_client, li_ion_sdo_client.current_mux, sdo_abort_code);
         li_ion_sdo_finalize_transfer(RC_ERROR, li_ion_sdo_client.size, sdo_abort_code);
      }
      else {

         if (NULL == upload_destination)
         {
            /* user didn't specify where to store the received data, but the transfer was successful nevertheless */
            li_ion_sdo_finalize_transfer(RC_SUCCESS, upload_size, LI_ION_SDO_NO_ABORT_CODE);
         }
         else
         {
            /* user did specify where to store the received data, now check if that location is large enough */
            if (upload_destination_size >= upload_size)
            {
               memcpy(upload_destination, li_ion_sdo_memory, upload_size);
               li_ion_sdo_finalize_transfer(RC_SUCCESS, upload_size, LI_ION_SDO_NO_ABORT_CODE);
            }
            else
            {
               li_ion_sdo_finalize_transfer(RC_ERROR_BUFFER_FULL, U32_CONST(0), LI_ION_SDO_NO_ABORT_CODE);
            }
         }
      }
   }
   else if (SDO_DOWNLOAD == current_transfer_type)
   {
      li_ion_sdo_finalize_transfer(RC_SUCCESS, U32_CONST(0), LI_ION_SDO_NO_ABORT_CODE);
   }
   else
   {
      /* else-branch should never be taken - this is to comply with MISRA */
      li_ion_sdo_finalize_transfer(RC_ERROR, U32_CONST(0), LI_ION_SDO_NO_ABORT_CODE);
   }
}

/*
 * This function is called by the cosdoc unit if an SDO transfer was aborted by the Lithium-Ion battery management SDO
 * server. It ends the SDO transfer and passes the abort code to the application via the post-transfer callback.
 *
 * \param   data        Contains whatever pointer was passed to 'cosdoc_initialize' as parameter 'abort_data'.
 * \param   mux         The SDO object index whose SDO transfer was aborted.
 * \param   code        The abort code sent by the SDO server.
 * \return  RC_SUCCESS  always
 */
RC li_ion_sdo_abort_by_server(void *data, U32 mux, U32 code )
{
   UNREFERENCED(data);

   li_ion_sdo_finalize_transfer(RC_ERROR, U32_CONST(0), code);

   return RC_SUCCESS;
}

/*
* This function runs in the 10 ms task. It handles the SDO timeout counter and finished SDO transfers.
*/
void li_ion_sdo_10ms_callback()
{
   cosdoc_timer_service( &li_ion_sdo_client, LI_ION_SDO_TIMEOUT_100MS_TICK );

   /* check if an SDO transfer was started recently, which might be completed now */
   if (SDO_IDLE != current_transfer_type)
   {
      if (cosdoc_transmission_status_completed == cosdoc_get_status(&li_ion_sdo_client))
      {
         li_ion_sdo_finalize_completed_transfer();
      }
      else if (cosdoc_transmission_status_aborted == cosdoc_get_status(&li_ion_sdo_client))
      {
         U32 abort_code = LI_ION_SDO_NO_ABORT_CODE;
         memcpy(&abort_code, li_ion_sdo_memory, (size_t) 4);   /* get 4-byte abort code from SDO send buffer */
         li_ion_sdo_finalize_transfer(RC_ERROR, U32_CONST(0), abort_code);
      }
      else
      {
         /* do nothing in other cosdoc states */
      }
   }
   else
   {
      /* do nothing if there is no SDO transfer in progress */
   }
}

/*
* Checks whether an SDO transfer is currently in progress or if a new transfer could be started right now.
*
* \param    new_transfer_type    The new type of SDO transfer that should be started if the SDO client is not busy.
* \return   TRUE                 If a new SDO-transfer can not be started because the SDO client is already busy. <br/>
*           FALSE                If the SDO client is not busy and a new SDO-transfer can be started.
*/
STATIC_INLINE BO li_ion_sdo_transfer_is_busy(li_ion_sdo_progress_t new_transfer_type)
{
   /* don't send a new SDO request if another transfer is currently in progress */
   if (SDO_IDLE != current_transfer_type)
   {
      return TRUE;
   }
   else
   {
      current_transfer_type = new_transfer_type;
      return FALSE;
   }

   /*
    * TODO:
    * current_transfer_type must be used as a 'real' semaphore (e.g. using an atomic test-and-set instruction) if this
    * SDO-transfer module should be made thread-safe. For now, a non-atomic test-and-set is OK, because this function
    * is only ever called from within the same task (10 ms task).
    */
}

/* Generic SDO send (download) function
*
* This function sends (downloads) some bytes to the lithium-ion battery management system SDO server, using the
* supplied object ID. It also stores a callback function pointer which will be called later after the SDO transfer is
* completed.
*
* It is safe to pass NULL for the callback.
* It is safe ifto pass a pointer to a local variable for 'data', because 'data' will not be used after the function returns.
*
* \param    post_transfer_callback  Will be called after the SDO response from the SDO server is processed.
* \param    object_id               The SDO object ID to which the data shall be downloaded.
* \param    data                    Pointer to the memory area which contains the data that shall be downloaded.
* \param    size                    Amount of bytes that shall be downloaded.
* \return   RC_SUCCESS              If the SDO request could be sent successfully. <br/>
*           RC_ERROR_BUSY           If another SDO transfer is currently in progress. <br/>
*           RC_ERROR_NULL           If 'data' is a NULL pointer. <br/>
*           RC_ERROR_MEMORY         If 'size' is too large to store the data in the SDO output buffer (see LI_ION_SDO_MAX_OBJECT_SIZE)
*/
STATIC_INLINE RC li_ion_sdo_download(li_ion_sdo_callback_t post_transfer_callback, const U32 object_id, const U8 * const data, const U8 size)
{
   RC return_code = RC_ERROR;

   if (TRUE == li_ion_sdo_transfer_is_busy(SDO_DOWNLOAD))
   {
      return_code = RC_ERROR_BUSY;
   }
   else
   {
      if ( LI_ION_SDO_MAX_OBJECT_SIZE < size )
      {
         return_code = RC_ERROR_MEMORY;               /* SDO object doesn't fit into the output buffer */
      }
      else
      {
         if (NULL == data)
         {
            return_code = RC_ERROR_NULL;
         }
         else
         {
            memcpy(li_ion_sdo_memory_handle.data, data, size);
            return_code = cosdoc_download( &li_ion_sdo_client, object_id, size, &li_ion_sdo_memory_handle.fd );

            if (RC_SUCCESS == return_code)
            {
               /* store information which is needed after the SDO transfer will be completed */
               finishing_callback = post_transfer_callback;
            }
            else
            {
               current_transfer_type = SDO_IDLE;
            }
         }
      }
   }

   return return_code;
}

/* Generic SDO request (upload) function
*
* This function requests to upload an SDO object from the lithium-ion battery management system SDO server. It stores a
* pointer to a buffer where the received object will be stored. It also stores a callback function pointer which will
* be called later after the SDO transfer is completed.
*
* It is safe to pass NULL for 'post_transfer_callback' or 'data'.
*
* \param    post_transfer_callback  Will be called after the SDO response from the SDO server is processed.
* \param    object_id               The SDO object ID of which the data shall be uploaded.
* \param    data                    Pointer to the memory area where the uploaded data shall be stored.
* \param    size                    Size of 'data'.
* \param    force_exact_size        If TRUE, the post-transmit callback will return RC_ERROR_RANGE if the size of the uploaded data does not equal 'size'.
* \return   RC_SUCCESS              If the SDO request could be sent successfully. <br/>
*           RC_ERROR_BUSY           If another SDO transfer is currently in progress. <br/>
*/
STATIC_INLINE RC li_ion_sdo_upload(li_ion_sdo_callback_t post_transfer_callback, const U32 object_id, U8 * const data, const U8 size, const BO force_exact_size)
{
   RC return_code = RC_ERROR;

   if (TRUE == li_ion_sdo_transfer_is_busy(SDO_UPLOAD))
   {
      return_code = RC_ERROR_BUSY;
   }
   else
   {
      memset(li_ion_sdo_memory, U8_CONST(0), LI_ION_SDO_MAX_OBJECT_SIZE);
      return_code = cosdoc_upload( &li_ion_sdo_client, object_id, &li_ion_sdo_memory_handle.fd );

      if (RC_SUCCESS == return_code)
      {
         /* store information which is needed after the SDO transfer will be completed */
         finishing_callback = post_transfer_callback;
         upload_destination = data;
         upload_destination_size = size;
         upload_size_exact = force_exact_size;
      }
      else
      {
         current_transfer_type = SDO_IDLE;
      }
   }

   return return_code;
}

/*===============================================[ public functions  ]================================================*/

void li_ion_sdo_initialize()
{
   /* initialize all status variables of the module */
   current_transfer_type = SDO_IDLE;
   finishing_callback = NULL;
   upload_destination = NULL;
   upload_destination_size = 0;
   upload_size_exact = FALSE;

   /* initialize storage for sent/received Li-Ion SDO objects */
   fdmem_initialize( &li_ion_sdo_memory_handle  , li_ion_sdo_memory, U32_CONST(0), LI_ION_SDO_MAX_OBJECT_SIZE);

   /* initialize SDO client object */
   cosdoc_initialize( &li_ion_sdo_client, li_ion_sdo_client_transmit, NULL, li_ion_sdo_abort_by_server, NULL, LI_ION_SDO_TIMEOUT_100MS );

   /* setup tasklets for the SDO transfer */
   tasklet_assign( sched_get_slice_idle(), &li_ion_sdo_idle_tasklet , li_ion_sdo_idle_callback, NULL );
   tasklet_assign( sched_get_slice_10ms(), &li_ion_sdo_10ms_tasklet , li_ion_sdo_10ms_callback, NULL );
}

void li_ion_sdo_client_receive(U8 msg[8])
{
   cosdoc_process(&li_ion_sdo_client, &msg[0]);
}

/*===============================================[ public functions  ]================================================*/
/* SDO downloads */

RC li_ion_sdo_send_nominal_voltage(li_ion_sdo_callback_t post_transfer_callback, const U8 nominal_voltage)
{
   return li_ion_sdo_download(post_transfer_callback, LI_ION_SDO_ID_NOMINAL_VOLTAGE, &nominal_voltage, U8_CONST(1));
}

RC li_ion_sdo_send_can_scheme(li_ion_sdo_callback_t post_transfer_callback, const li_ion_sdo_can_scheme_t can_scheme)
{
   return li_ion_sdo_download(post_transfer_callback, LI_ION_SDO_ID_CAN_SCHEME, &can_scheme, U8_CONST(1));
}

RC li_ion_sdo_send_VIN(li_ion_sdo_callback_t post_transfer_callback, const CH * const vin, const U8 vin_length)
{
   return li_ion_sdo_download(post_transfer_callback, LI_ION_SDO_ID_VIN, (U8*) vin, vin_length);
}

RC li_ion_sdo_turn_battery_on(li_ion_sdo_callback_t post_transfer_callback)
{
   return li_ion_sdo_download(post_transfer_callback, LI_ION_SDO_ID_TURN_BATTERY_ON, &li_ion_sdo_one, U8_CONST(1));
}

RC li_ion_sdo_turn_battery_off(li_ion_sdo_callback_t post_transfer_callback)
{
   return li_ion_sdo_download(post_transfer_callback, LI_ION_SDO_ID_TURN_BATTERY_OFF, &li_ion_sdo_one, U8_CONST(1));
}

RC li_ion_sdo_reset_battery(li_ion_sdo_callback_t post_transfer_callback)
{
   return li_ion_sdo_download(post_transfer_callback, LI_ION_SDO_ID_RESET_BATTERY, &li_ion_sdo_one, U8_CONST(1));
}

/*===============================================[ public functions  ]================================================*/
/* SDO uploads */

RC li_ion_sdo_request_battery_serial_number(li_ion_sdo_callback_t post_transfer_callback, U32 * const serial_number)
{
   return li_ion_sdo_upload(post_transfer_callback, LI_ION_SDO_ID_BATTERY_SERIAL_NO, (U8*) serial_number, U8_CONST(4), TRUE);
}

RC li_ion_sdo_request_battery_energy(li_ion_sdo_callback_t post_transfer_callback, U32 * const energy_content)
{
   return li_ion_sdo_upload(post_transfer_callback, LI_ION_SDO_ID_ENERGY_CONTENT, (U8*) energy_content, U8_CONST(4), TRUE);
}

RC li_ion_sdo_request_software_variant(li_ion_sdo_callback_t post_transfer_callback, CH * const buffer, const U8 buffer_size)
{
   return li_ion_sdo_upload(post_transfer_callback, LI_ION_SDO_ID_SOFTWARE_VARIANT, (U8*) buffer, buffer_size, FALSE);
}

RC li_ion_sdo_request_hardware_variant(li_ion_sdo_callback_t post_transfer_callback, CH * const buffer, const U8 buffer_size)
{
   return li_ion_sdo_upload(post_transfer_callback, LI_ION_SDO_ID_HARDWARE_VARIANT, (U8*) buffer, buffer_size, FALSE);
}

/***********************************************************************************************************************
*                             Copyright 2014 Linde Material Handling. All rights reserved.                             *
***********************************************************************************************************************/
